Skip to content

Add WebSocket vital event schemas - #429

Draft
bdibon wants to merge 2 commits into
masterfrom
boris.dibon/add-websocket-vital-schema
Draft

Add WebSocket vital event schemas#429
bdibon wants to merge 2 commits into
masterfrom
boris.dibon/add-websocket-vital-schema

Conversation

@bdibon

@bdibon bdibon commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What this adds

A websocket vital type, reporting a WebSocket connection as a stream of vitals over its lifetime — one per connection phase, all sharing a vital.websocket.id that the backend reducer groups on. vital.websocket.id is the connection id and is deliberately not vital.id, which stays a fresh UUID per event.

Four phase schemas over a shared common schema, plus two shared fragments:

File Role
_vital-websocket-common-schema.json vital.type const websocket, required vital.name, and vital.websocket.id — the only websocket field required on all four phases
vital-websocket-connecting-schema.json requires url, connecting_date
vital-websocket-open-schema.json requires the handshake fields, snapshot_version, snapshot
vital-websocket-closing-schema.json requires closing_date, close_initiator
vital-websocket-closed-schema.json requires closed_date, duration, tracking_end_reason, snapshot_version
_vital-websocket-snapshot-schema.json snapshot.inbound / snapshot.outbound, shared by the open and closed phases
_vital-websocket-message-direction-schema.json the per-direction message statistics, $ref'd by both directions

Registered in the RumVitalEvent oneOf of rum-events-schema.json and rum-events-browser-schema.json. Not registered in rum-events-mobile-schema.json — the browser SDK is the only SDK reporting these today. That does not keep them away from mobile, though: per the README the Android SDK generates models from everything under schemas/rum/, which the CI run on this PR confirmed.

How the four phases are discriminated

By their disjoint required sets, not by vital.name. The natural design would put a const on vital.name in each phase schema. That cannot work: dd-sdk-android's generator cannot narrow a property a shared schema already declares — Primitive.mergedWith accepts only an identical Primitive, and both Constant.mergedWith and Enum.mergedWith error unconditionally (TypeDefinition.kt). Since _vital-common-schema.json declares vital.name as a free-form string, a const or an enum on it fails generateRumModelsFromJson with Can't merge Primitive with type Constant.

So vital.name is required but unconstrained, and each phase schema records the value it is reported with in its description. Consequences reviewers should weigh:

  • A payload whose vital.name disagrees with its field set validates — the name is not checked against the phase.
  • A payload satisfying two phases' required sets is rejected, since oneOf demands exactly one match.
  • The four generated TypeScript types are not a discriminated union, so a consumer holding a RumEvent cannot narrow to a phase by vital.name. Per-phase builders that annotate their return type are unaffected.

If the Android limitation is lifted, adding the four consts back is a small change here.

Judgement calls worth pushing back on

1. Four per-phase schemas rather than one all-optional vital.websocket object. The alternative was a single schema with every field but id optional and the per-phase presence rules left to descriptions. Per-phase files make each phase's required set machine-checked instead of prose. RumVitalEvent goes from 3 arms to 7 as a result.

2. Per-phase required sets, against the README's "new fields must not be set as required". That rule protects existing event shapes: no historical event has vital.type: "websocket", so nothing old can start failing. The forward cost is that these sets now constrain the SDK — relaxing one later is a breaking change. timeseries-memory-schema.json and timeseries-cpu-schema.json require six fields each on the same reasoning.

3. vital.websocket.id carries the same UUID pattern as vital.id. Tightens the reducer's grouping key to a UUID.

4. The close_event-only rule for close_code / close_reason / was_clean is description-only. An if/then expressing it is rejected by ajv's strictRequired in yarn validate unless the three properties are re-declared inside then; the duplication wasn't worth it.

5. The per-phase split stops at the snapshot. silence_before_close and buffered_amount_at_close are CLOSED-only but live in the shared snapshot fragments as optional, so they would also validate on an OPEN heartbeat. Splitting the snapshot per phase costs two more files and produces messier generated types.

6. Nothing here rejects an unknown field. The rum/ schemas don't set additionalProperties: false, and it isn't usable with this composition anyway — in allOf each arm validates the whole object, so an arm forbidding extras would reject vital.websocket.id from the sibling common arm. Consequence: fields deliberately not part of this model still validate if emitted — closing_duration on CLOSED (the reducer computes it as closed_date − closing_date), close_initiator on CLOSED (the reducer derives it), and ready_state. They are absent from the schema rather than rejected by it.

Test instructions

yarn && yarn build && yarn validate && yarn format

Six samples cover all four phases, plus a CLOSED with tracking_end_reason: session_end (no CloseEvent fields) and a CLOSED for a connection that never opened (no snapshot). The pre-existing samples/rum-events/vital.json still validates, which is what confirms the oneOf resolves to exactly one arm per event.

Separately verified: the four sample payloads published in the WebSocket Data Model RFC validate against these schemas unchanged, and each of the six new samples matches exactly one phase arm while the existing duration vital matches none.

bdibon and others added 2 commits August 13, 2026 12:05
Report WebSocket connection metrics as a stream of vital events, one per
connection phase, all sharing a `vital.websocket.id`. Adds one schema
per
phase, discriminated by a const on `vital.name` over the shared
`vital.type` const `websocket`, following the pattern the timeseries
event schemas already use.

Registered in `rum-events-schema.json` and
`rum-events-browser-schema.json`
only: the browser SDK is the only SDK reporting these for now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`dd-sdk-android`'s model generator cannot narrow a property that a shared
schema already declares: `Primitive.mergedWith` accepts only an identical
`Primitive`, while `Constant.mergedWith` and `Enum.mergedWith` error
unconditionally (`buildSrc/.../jsonschema/TypeDefinition.kt`). Since
`_vital-common-schema.json` declares `vital.name` as a free-form string, a
`const` on it in each phase schema fails `generateRumModelsFromJson` with
"Can't merge Primitive with type Constant".

Drop the consts. The four phase schemas stay mutually exclusive through
their disjoint `required` sets, so `oneOf` still resolves to exactly one arm
per event. `vital.name` remains required, declared as the plain string the
shared envelope already uses, and each phase schema records the value it is
reported with in its description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant